#include <iostream>
#include <stdint.h>
#include <cstring>
#include <cstdio>
#include <fstream>

using namespace std;

#include <sys/time.h>
#include <stdlib.h>
#include "HashingStrategy.hpp"

//#define MD5
//#define SHA1

#ifdef MD5
    #include "MD5hashing.hpp"
    #define _MD5_DIGEST_SIZE    16    
    #define _DIGEST_SIZE _MD5_DIGEST_SIZE
#elif SHA1
    #include "SHA1hashing.hpp"
    #define _SHA1_DIGEST_SIZE   20
    #define _DIGEST_SIZE _SHA1_DIGEST_SIZE
#endif

static void print_usage(char* prog_name);
static void process_file(HashingStrategy* context, char* filename);
static void process_stdin(HashingStrategy* context);

struct timeval endTime, startTime;
long total;

int main(int argc, char* argv[]) {
    
    uint8_t digest[_DIGEST_SIZE];

#ifdef MD5
    HashingStrategy* hashingContext = new MD5hashing();
#elif SHA1
    HashingStrategy* hashingContext = new SHA1hashing();
#endif

    if(argc > 1 && argc < 4) {
        if (strncmp (argv[1], "-s", 2) == 0) {
            total = strlen(argv[2]);
            gettimeofday(&startTime, NULL);
            hashingContext->process ((const uint8_t*) argv[2], total);
            gettimeofday(&endTime, NULL);
        } else if(strlen (argv[1]) > 0) {
            process_file(hashingContext, argv[1]);
        } else {
            process_stdin(hashingContext);
        }
    } else {
        print_usage(argv[0]);
        delete hashingContext;
        return 0;
    }

    /*********************/
    /* hash finalization */
    /*********************/
    hashingContext->finalize();

    /**************/
    /* Displaying */
    /**************/

    /* First way to print the result
     * */
    hashingContext->get_digest(digest, _DIGEST_SIZE);
    for(unsigned int i=0; i<_DIGEST_SIZE; i++) {
        printf("%02x", digest[i]);
    }
    cout << endl;

    /* Second way to print the result
     **/
    //cout << "hash [using to_string()] : " << hashingContext->to_string() << endl;

    /* Third way to print the result
     **/
    //cout << "hash [using operator <<] : " << *hashingContext << endl;

    long double d = (endTime.tv_sec + 1e-6 * endTime.tv_usec) - (startTime.tv_sec + 1e-6 * startTime.tv_usec);
    cout << "\nTime = " << d << "s" << endl;
    cout << "Speed = " << total / d << " bytes/s" << endl;

    delete hashingContext;

    return 0;
}

/* Print usage
 * */
static void print_usage(char* prog_name) {
    cerr << "Usage : " << prog_name << " [-s string] | [filename] | (none)" << endl;
    cerr << "\t-s\t\t- digests string" << endl;
    cerr << "\tfilename\t- digests file" << endl;
    cerr << "\t(none)\t- digests standard input" << endl;
}

/* Digests a file.
 * */
static void process_file (HashingStrategy* context, char* filename)
{
    std::ifstream is(filename, std::ifstream::binary);

    if (is.is_open()) {
        uint8_t buffer[1024];
        size_t r;
        gettimeofday(&startTime, NULL);
        
        while (!is.eof()) {
            is.read(reinterpret_cast<char*>(buffer), 1024);
            r = is.gcount();
            total += r;
            context->process(buffer, r);
        }
        
        gettimeofday(&endTime, NULL);
        is.close();

    } else {
        cerr << filename << " can't be opened" << endl;
    }
}

/* Digest standard input.
 * */
static void process_stdin(HashingStrategy* context)
{
    uint8_t buffer[1024];
    size_t r;
    gettimeofday(&startTime, NULL);
    
    while (!cin.eof()) {
        cin.read(reinterpret_cast<char*>(buffer), 1024);
        r = cin.gcount();
        total += r;
        context->process(buffer, r);
    }
    
    gettimeofday(&endTime, NULL);
}
